//
//  SLClassInfo.m
//  SLModel
//
//  Created by S_Line on 2018/6/21.
//  Copyright © 2018年 S_Line. All rights reserved.
//

#import "SLClassInfo.h"

SLEncodingType SLEncodingGetType(const char *typeEncoding) {
    char *type = (char *)typeEncoding;
    if (type == nil) {
        return SLEncodingTypeUnknown;
    }
    size_t len = strlen(type);
    if (len == 0) {
        return SLEncodingTypeUnknown;
    }
    
    SLEncodingType qualifier = 0;
    bool prefix = true;
    while (prefix) {
        switch (*type) {
            case 'r':
                qualifier |= SLEncodingTypeQualifierConst;
                type++;
                break;
            case 'R':
                qualifier |= SLEncodingTypeQualifierByRef;
                type++;
                break;
            case 'n':
                qualifier |= SLEncodingTypeQualifierIn;
                type++;
                break;
            case 'N':
                qualifier |= SLEncodingTypeQualifierInOut;
                type++;
                break;
            case 'o':
                qualifier |= SLEncodingTypeQualifierOut;
                type++;
                break;
            case 'O':
                qualifier |= SLEncodingTypeQualifierBycopy;
                type++;
                break;
            case 'V':
                qualifier |= SLEncodingTypeQualifierOneway;
                type++;
                break;
            default:
                prefix = false;
                break;
        }
    }
    
    len = strlen(type);
    if (len == 0) {
        return SLEncodingTypeUnknown | qualifier;
    }
    
    switch (*type) {
        case 'v':
            return SLEncodingTypeVoid | qualifier;
            break;
        case 'B':
            return SLEncodingTypeBool | qualifier;
            break;
        case 'c':
            return SLEncodingTypeInt8 | qualifier;
            break;
        case 'C':
            return SLEncodingTypeUInt8 | qualifier;
            break;
        case 's':
            return SLEncodingTypeInt16 | qualifier;
            break;
        case 'S':
            return SLEncodingTypeUInt16 | qualifier;
            break;
        case 'i':
            return SLEncodingTypeInt32 | qualifier;
            break;
        case 'I':
            return SLEncodingTypeUInt32 | qualifier;
            break;
        case 'l':
            return SLEncodingTypeInt32 | qualifier;
            break;
        case 'L':
            return SLEncodingTypeUInt32 | qualifier;
            break;
        case 'q':
            return SLEncodingTypeInt64 | qualifier;
            break;
        case 'Q':
            return SLEncodingTypeUInt64 | qualifier;
            break;
        case 'f':
            return SLEncodingTypeFloat | qualifier;
            break;
        case 'd':
            return SLEncodingTypeDouble | qualifier;
            break;
        case 'D':
            return SLEncodingTypeLongDouble | qualifier;
            break;
        case '#':
            return SLEncodingTypeClass | qualifier;
            break;
        case ':':
            return SLEncodingTypeSel | qualifier;
            break;
        case '*':
            return SLEncodingTypeCString | qualifier;
            break;
        case '^':
            return SLEncodingTypePointer | qualifier;
            break;
        case '[':
            return SLEncodingTypeCArray | qualifier;
            break;
        case '(':
            return SLEncodingTypeUnion | qualifier;
            break;
        case '{':
            return SLEncodingTypeStruct | qualifier;
            break;
        case '@':
            if (len == 2 && *(type + 1) == '?') {
                return SLEncodingTypeBlock | qualifier;
            } else {
                return SLEncodingTypeObject | qualifier;
            }
            break;
        default:
            return SLEncodingTypeUnknown | qualifier;
            break;
    }
}

@implementation SLClassIvarInfo

- (instancetype)initWithIvar:(Ivar)ivar {
    if (!ivar) return nil;
    self = [super init];
    
    if (self) {
        _ivar = ivar;
        
        const char *name = ivar_getName(ivar);
        if (name) {
            _name = [NSString stringWithUTF8String:name];
        }
        
        _offset = ivar_getOffset(ivar);
        
        const char *typeEncoding = ivar_getTypeEncoding(ivar);
        if (typeEncoding) {
            _typeEncoding = [NSString stringWithUTF8String:typeEncoding];
            _type = SLEncodingGetType(typeEncoding);
        }
    }
    
    return self;
}

@end

@implementation SLClassMethodInfo

- (instancetype)initWithMethod:(Method)method {
    if (!method) {
        return nil;
    }
    
    self = [super init];
    
    if (self) {
        _method = method;
        _sel = method_getName(method);
        _imp = method_getImplementation(method);
        
        const char *name = sel_getName(_sel);
        if (name) {
            _name = [NSString stringWithUTF8String:name];
        }
        
        const char *typeEncoding = method_getTypeEncoding(method);
        if (typeEncoding) {
            _typeEncoding = [NSString stringWithUTF8String:typeEncoding];
        }
        
        char *returnType = method_copyReturnType(method);
        if (returnType) {
            _returnTypeEncoding = [NSString stringWithUTF8String:returnType];
        }
        
        unsigned int argumentCount = method_getNumberOfArguments(method);
        if (argumentCount > 0) {
            NSMutableArray *argumentTypes = [NSMutableArray array];
            for (unsigned int i = 0; i < argumentCount; i++) {
                char *argumentType = method_copyArgumentType(method, i);
                NSString *type = argumentType ? [NSString stringWithUTF8String:argumentType] : nil;
                [argumentTypes addObject:type];
                if (argumentType) {
                    free(argumentType);
                }
            }
            _argumentTypeEncodings = argumentTypes;
        }
    }
    
    return self;
}

@end

@implementation SLClassPropertyInfo

- (instancetype)initWithProperty:(objc_property_t)property {
    if (!property) {
        return nil;
    }
    
    self = [super init];
    
    if (self) {
        _property = property;
        
        const char *name = property_getName(property);
        if (name) {
            _name = [NSString stringWithUTF8String:name];
        }
        
        SLEncodingType type = 0;
        unsigned int attrCount;
        objc_property_attribute_t *attrs = property_copyAttributeList(property, &attrCount);
        for (unsigned int i = 0; i<attrCount; i++) {
            switch (attrs[i].name[0]) {
                case 'T':
                    if (attrs[i].value) {
                        _typeEncoding = [NSString stringWithUTF8String:attrs[i].value];
                        type = SLEncodingGetType(attrs[i].value);
                        
                        if ((type & SLEncodingTypeMask) == SLEncodingTypeObject && _typeEncoding.length ) {
                            NSScanner *scanner = [NSScanner scannerWithString:_typeEncoding];
                            if (![scanner scanString:@"@\"" intoString:NULL]) {
                                continue;
                            }
                            
                            NSString *clsName = nil;
                            if ([scanner scanUpToCharactersFromSet:[NSCharacterSet characterSetWithCharactersInString:@"\"<"] intoString:&clsName]) {
                                if (clsName.length) {
                                    _cls = objc_getClass(clsName.UTF8String);
                                }
                            }
                            
                            NSMutableArray *protocols = nil;
                            while ([scanner scanString:@"<" intoString:NULL]) {
                                NSString *protocol = nil;
                                if ([scanner scanUpToString:@">" intoString:&protocol]) {
                                    if (protocol.length) {
                                        if (!protocols) {
                                            protocols = [NSMutableArray new];
                                            [protocols addObject:protocol];
                                        }
                                    }
                                }
                                [scanner scanString:@">" intoString:NULL];
                            }
                            _protocols = protocols;
                        }
                    }
                    break;
                case 'V':
                    if (attrs[i].value) {
                        _ivarName = [NSString stringWithUTF8String:attrs[i].value];
                    }
                    break;
                case 'R':
                    type |= SLEncodingTypePropertyReadonly;
                    break;
                case 'C':
                    type |= SLEncodingTypePropertyCopy;
                    break;
                case '&':
                    type |= SLEncodingTypePropertyRetain;
                    break;
                case 'N':
                    type |= SLEncodingTypePropertyNonatomic;
                    break;
                case 'D':
                    type |= SLEncodingTypePropertyDynamic;
                    break;
                case 'W':
                    type |= SLEncodingTypePropertyWeak;
                    break;
                case 'G':
                    type |= SLEncodingTypePropertyCustomGetter;
                    if (attrs[i].value) {
                        _getter = NSSelectorFromString([NSString stringWithUTF8String:attrs[i].value]);
                    }
                    break;
                case 'S':
                    type |= SLEncodingTypePropertyCustomSetter;
                    if (attrs[i].value) {
                        _setter = NSSelectorFromString([NSString stringWithUTF8String:attrs[i].value]);
                    }
                    break;
                default:
                    break;
            }
        }
        
        if (attrs) {
            free(attrs);
            attrs = NULL;
        }
        
        _type = type;
        if (_name.length) {
            if (!_getter) {
                NSString *getterStr = _name;
                _getter = NSSelectorFromString(getterStr);
            }
            if (!_setter) {
                NSString *setterStr = [NSString stringWithFormat:@"set%@%@:", [_name substringToIndex:1].uppercaseString, [_name substringFromIndex:1]];
                _setter = NSSelectorFromString(setterStr);
            }
        }
    }
    
    return self;
}

@end

@implementation SLClassInfo {
    BOOL _needUpdate;
}

- (instancetype)initWithClass:(Class)cls {
    if (!cls) {
        return nil;
    }
    
    self = [super init];
    
    if (self) {
        _cls = cls;
        _superCls = class_getSuperclass(cls);
        _isMeta = class_isMetaClass(cls);
        if (!_isMeta) {
            _metaCls = objc_getMetaClass(class_getName(cls));
        }
        _name = NSStringFromClass(cls);
        [self _update];
        
        _superCls = [self.class classInfoWithClass:_superCls];
    }
    
    return self;
}

- (void)_update {
    _ivarInfos = nil;
    _methodInfos = nil;
    _propertyInfos = nil;
    
    Class cls = self.cls;
    unsigned int methodCount = 0;
    Method *methods = class_copyMethodList(cls, &methodCount);
    if (methods) {
        NSMutableDictionary *methodInfos = [NSMutableDictionary new];
        _methodInfos = methodInfos;
        for (unsigned int i = 0; i < methodCount; i++) {
            SLClassMethodInfo *methodInfo = [[SLClassMethodInfo alloc] initWithMethod:methods[i]];
            if (methodInfo.name) {
                methodInfos[methodInfo.name] = methodInfo;
            }
        }
        free(methods);
    }
    
    unsigned int propertyCount = 0;
    objc_property_t *properties = class_copyPropertyList(cls, &propertyCount);
    if (properties) {
        NSMutableDictionary *propertyInfos = [NSMutableDictionary dictionary];
        _propertyInfos = propertyInfos;
        for (unsigned int i = 0; i < propertyCount; i++) {
            SLClassPropertyInfo *propertyInfo = [[SLClassPropertyInfo alloc]  initWithProperty:properties[i]];
            if (propertyInfo.name) {
                propertyInfos[propertyInfo.name] = propertyInfo;
            }
        }
        
        free(properties);
    }
    
    unsigned int ivarCount = 0;
    Ivar *ivars = class_copyIvarList(cls, &ivarCount);
    if (ivars) {
        NSMutableDictionary *ivarInfos = [NSMutableDictionary new];
        _ivarInfos = ivarInfos;
        for (unsigned int i = 0; i < ivarCount; i++) {
            SLClassIvarInfo *ivarInfo = [[SLClassIvarInfo alloc] initWithIvar:ivars[i]];
            if (ivarInfo.name) {
                ivarInfos[ivarInfo.name] = ivarInfo;
            }
        }
        
        free(ivars);
    }
    
    if (!_ivarInfos) {
        _ivarInfos = @{};
    }
    if (!_propertyInfos) {
        _propertyInfos = @{};
    }
    if (!_methodInfos) {
        _methodInfos = @{};
    }
    
    _needUpdate = false;
}

- (void)setNeedUpdate {
    _needUpdate = true;
}

- (BOOL)needUpdate {
    return _needUpdate;
}

+ (instancetype)classInfoWithClass:(Class)cls {
    if (!cls) {
        return nil;
    }
    
    static CFMutableDictionaryRef classCache;
    static CFMutableDictionaryRef metaCache;
    static dispatch_semaphore_t lock;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        classCache = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
        metaCache = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
        lock = dispatch_semaphore_create(1);
    });
    dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
    SLClassInfo *classInfo = CFDictionaryGetValue(class_isMetaClass(cls) ? metaCache : classCache,  (__bridge const void *)(cls));
    if (classInfo && classInfo->_needUpdate) {
        [classInfo _update];
    }
    dispatch_semaphore_signal(lock);
    if (!classInfo) {
        classInfo = [[SLClassInfo alloc] initWithClass:cls];
        dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
        CFDictionarySetValue(classInfo.isMeta ? metaCache : classCache, (__bridge const void *)(cls), (__bridge const void *)(classInfo));
        dispatch_semaphore_signal(lock);
    }
    
    return classInfo;
}

+ (instancetype)classInfoWithClassName:(NSString *)className {
    Class cls = NSClassFromString(className);
    return [self classInfoWithClass:cls];
}

@end
